package com.brashmonkey.spriter;

import com.brashmonkey.spriter.Mainline.Key;
import com.brashmonkey.spriter.Mainline.Key.BoneRef;
import com.brashmonkey.spriter.Mainline.Key.ObjectRef;
import com.brashmonkey.spriter.Timeline.Key.Bone;
import com.brashmonkey.spriter.Timeline.Key.Object;

import java.util.HashMap;

/**
 * Represents an animation of a Spriter SCML file.
 * An animation holds {@link Timeline}s and a {@link Mainline} to animate objects.
 * Furthermore it holds an {@link #id}, a {@link #length}, a {@link #name} and whether it is {@link #looping} or not.
 *
 * @author Trixt0r
 */
public class Animation {

	public final Mainline mainline;
	private final Timeline[] timelines;
	private int timelinePointer = 0;
	private final HashMap<String, Timeline> nameToTimeline;
	public final int id, length;
	public final String name;
	public final boolean looping;
	Key currentKey;
	Timeline.Key[] tweenedKeys, unmappedTweenedKeys;
	private boolean prepared;

	public Animation(Mainline mainline, int id, String name, int length, boolean looping, int timelines) {
		this.mainline = mainline;
		this.id = id;
		this.name = name;
		this.length = length;
		this.looping = looping;
		this.timelines = new Timeline[timelines];
		this.prepared = false;
		this.nameToTimeline = new HashMap<String, Timeline>();
		//this.currentKey = mainline.getKey(0);
	}

	/**
	 * Returns a {@link Timeline} with the given index.
	 *
	 * @param index the index of the timeline
	 * @return the timeline with the given index
	 * @throws IndexOutOfBoundsException if the index is out of range
	 */
	public Timeline getTimeline(int index) {
		return this.timelines[index];
	}

	/**
	 * Returns a {@link Timeline} with the given name.
	 *
	 * @param name the name of the time line
	 * @return the time line with the given name or null if no time line exists with the given name.
	 */
	public Timeline getTimeline(String name) {
		return this.nameToTimeline.get(name);
	}

	void addTimeline(Timeline timeline) {
		this.timelines[timelinePointer++] = timeline;
		this.nameToTimeline.put(timeline.name, timeline);
	}

	/**
	 * Returns the number of time lines this animation holds.
	 *
	 * @return the number of time lines
	 */
	public int timelines() {
		return timelines.length;
	}

	public String toString() {
		String toReturn = getClass().getSimpleName() + "|[id: " + id + ", " + name + ", duration: " + length + ", is looping: " + looping;
		toReturn += "Mainline:\n";
		toReturn += mainline;
		toReturn += "Timelines\n";
		for (Timeline timeline : this.timelines)
			toReturn += timeline;
		toReturn += "]";
		return toReturn;
	}

	/**
	 * Updates the bone and object structure with the given time to the given root bone.
	 *
	 * @param time The time which has to be between 0 and {@link #length} to work properly.
	 * @param root The root bone which is not allowed to be null. The whole animation runs relative to the root bone.
	 */
	public void update(int time, Bone root) {
		if (!this.prepared)
			throw new SpriterException("This animation is not ready yet to animate itself. Please call prepare()!");
		if (root == null)
			throw new SpriterException("The root can not be null! Set a root bone to apply this animation relative to the root bone.");
		this.currentKey = mainline.getKeyBeforeTime(time);

		for (Timeline.Key timelineKey : this.unmappedTweenedKeys)
			timelineKey.active = false;
		for (BoneRef ref : currentKey.boneRefs)
			this.update(ref, root, time);
		for (ObjectRef ref : currentKey.objectRefs)
			this.update(ref, root, time);
	}

	protected void update(BoneRef ref, Bone root, int time) {
		boolean isObject = ref instanceof ObjectRef;
		//Get the timelines, the refs pointing to
		Timeline timeline = getTimeline(ref.timeline);
		Timeline.Key key = timeline.getKey(ref.key);
		Timeline.Key nextKey = timeline.getKey((ref.key + 1) % timeline.keys.length);
		int currentTime = key.time;
		int nextTime = nextKey.time;
		if (nextTime < currentTime) {
			if (!looping) nextKey = key;
			else nextTime = length;
		}
		//Normalize the time
		float t = (float) (time - currentTime) / (float) (nextTime - currentTime);
		if (Float.isNaN(t) || Float.isInfinite(t)) t = 1f;
		if (currentKey.time > currentTime) {
			float tMid = (float) (currentKey.time - currentTime) / (float) (nextTime - currentTime);
			if (Float.isNaN(tMid) || Float.isInfinite(tMid)) tMid = 0f;
			t = (float) (time - currentKey.time) / (float) (nextTime - currentKey.time);
			if (Float.isNaN(t) || Float.isInfinite(t)) t = 1f;
			t = currentKey.curve.tween(tMid, 1f, t);
		} else
			t = currentKey.curve.tween(0f, 1f, t);
		//Tween bone/object
		Bone bone1 = key.object();
		Bone bone2 = nextKey.object();
		Bone tweenTarget = this.tweenedKeys[ref.timeline].object();
		if (isObject) this.tweenObject((Object) bone1, (Object) bone2, (Object) tweenTarget, t, key.curve, key.spin);
		else this.tweenBone(bone1, bone2, tweenTarget, t, key.curve, key.spin);
		this.unmappedTweenedKeys[ref.timeline].active = true;
		this.unmapTimelineObject(ref.timeline, isObject, (ref.parent != null) ?
				this.unmappedTweenedKeys[ref.parent.timeline].object() : root);
	}

	void unmapTimelineObject(int timeline, boolean isObject, Bone root) {
		Bone tweenTarget = this.tweenedKeys[timeline].object();
		Bone mapTarget = this.unmappedTweenedKeys[timeline].object();
		if (isObject) ((Object) mapTarget).set((Object) tweenTarget);
		else mapTarget.set(tweenTarget);
		mapTarget.unmap(root);
	}

	protected void tweenBone(Bone bone1, Bone bone2, Bone target, float t, Curve curve, int spin) {
		target.angle = curve.tweenAngle(bone1.angle, bone2.angle, t, spin);
		curve.tweenPoint(bone1.position, bone2.position, t, target.position);
		curve.tweenPoint(bone1.scale, bone2.scale, t, target.scale);
		curve.tweenPoint(bone1.pivot, bone2.pivot, t, target.pivot);
	}

	protected void tweenObject(Object object1, Object object2, Object target, float t, Curve curve, int spin) {
		this.tweenBone(object1, object2, target, t, curve, spin);
		target.alpha = curve.tweenAngle(object1.alpha, object2.alpha, t);
		target.ref.set(object1.ref);
	}

	Timeline getSimilarTimeline(Timeline t) {
		Timeline found = getTimeline(t.name);
		if (found == null && t.id < this.timelines()) found = this.getTimeline(t.id);
		return found;
	}
	
	/*Timeline getSimilarTimeline(BoneRef ref, Collection<Timeline> coveredTimelines){
		if(ref.parent == null) return null;
    	for(BoneRef boneRef: this.currentKey.objectRefs){
    		Timeline t = this.getTimeline(boneRef.timeline);
    		if(boneRef.parent != null && boneRef.parent.id == ref.parent.id && !coveredTimelines.contains(t))
    			return t;
    	}
    	return null;
	}
	
	Timeline getSimilarTimeline(ObjectRef ref, Collection<Timeline> coveredTimelines){
		if(ref.parent == null) return null;
    	for(ObjectRef objRef: this.currentKey.objectRefs){
    		Timeline t = this.getTimeline(objRef.timeline);
    		if(objRef.parent != null && objRef.parent.id == ref.parent.id && !coveredTimelines.contains(t))
    			return t;
    	}
    	return null;
	}*/

	/**
	 * Prepares this animation to set this animation in any time state.
	 * This method has to be called before {@link #update(int, Bone)}.
	 */
	public void prepare() {
		if (this.prepared) return;
		this.tweenedKeys = new Timeline.Key[timelines.length];
		this.unmappedTweenedKeys = new Timeline.Key[timelines.length];

		for (int i = 0; i < this.tweenedKeys.length; i++) {
			this.tweenedKeys[i] = new Timeline.Key(i);
			this.unmappedTweenedKeys[i] = new Timeline.Key(i);
			this.tweenedKeys[i].setObject(new Timeline.Key.Object(new Point(0, 0)));
			this.unmappedTweenedKeys[i].setObject(new Timeline.Key.Object(new Point(0, 0)));
		}
		if (mainline.keys.length > 0) currentKey = mainline.getKey(0);
		this.prepared = true;
	}

}
